# Enhanced posting bot
# Note that this isn't really an artificial intelligence, it just pretends to be
# by Cee Pee

import math
import os
import random
import re
import time
import forumapi

_phrases = {}
def get_phrases(name):
	"""Opens a phrases file and saves the data within it."""
	
	global _phrases
	_phrases[name] = open(forumapi.data_file(name, "phrases"), encoding="utf8").read()

def find_phrases():
	"""Returns a list of all phrases files."""
	
	phrases = []
	for file in os.listdir("data"):
		m = re.match("^(.*)\.phrases$", file)
		if m:
			phrases.append(m.group(1))
	
	return(phrases)

_phrases_files = find_phrases()

# TODO: Move these to whitelist and blacklist files
_whitelist_words = ["god"]
_blacklist_words = [
	"about", "according", "actually", "after", "again", "against", "almost",
	"alright", "also", "always", "among", "another", "anyway", "anyone",
	"anybody", "around", "aside",
	"back", "because", "been", "before", "being", "beside", "besides",
	"better", "between", "both",
	"come", "could",
	"does", "doing", "done", "down",
	"each", "either", "enough", "even", "ever", "every",
	"first", "from",
	"given", "going", "good", "great",
	"hate", "have", "having", "here", "however",
	"into",
	"just",
	"last", "least", "less", "like", "little", "look", "looks", "love",
	"made", "make", "many", "more", "most", "much", "must",
	"neither", "never", "nobody", "nothing",
	"once", "only", "other", "over",
	"people",
	"quite",
	"rather", "really", "right",
	"said", "same", "seem", "seen", "several", "should", "some", "something",
	"still", "such",
	"take", "than", "that", "them", "then", "there", "these", "they", "this",
	"those", "though", "thread", "through", "took",
	"under", "until",
	"very",
	"want", "well", "went", "were", "what", "whatever", "when", "where",
	"whether", "which", "while", "whole", "whom", "will", "with", "without",
	"would", "wrong",
	"your", 
]
def match_is_good(text):
	"""Figures out whether a word is a good match.
	Text with less than four characters are bad, unless they match a whitelist.
	Longer text is good, unless they match a blacklist."""
	
	if _whitelist_words.count(text) != 0:
		return(True)
	
	if len(text) < 4:
		return(False)
	
	if _blacklist_words.count(text) != 0:
		return(False)
	
	return(True)

def search_phrases(name, text):
	"""Searches the phrases file for the word `text`, and returns a line that
	has that word in it."""
	
	regex = re.compile("^((?:.*?\W)?{0}(?:\W.*?)?)$".format(re.escape(text)), re.M | re.I)
	result = regex.findall(_phrases[name])
	if result:
		ret = random.choice(result) + "\n\n(Other lines containing [b]{0}[/b]: {1})".format(text, len(result)-1)
		# We may have less memory leaks by deleting the result beforehand
		del result
		return(ret)
	else:
		return(False)

def bolden_match(match):
	"""Helper function that boldens everything in the quote that matches."""
	
	return("[b]" + match.group(1) + "[/b]")

def settings(settings):
	settings.add_option("-f", "--phrases", type="choice", choices=_phrases_files, help="phrases file", metavar="FILE")
	settings.add_option("-t", "--threads", type="list", help="thread IDs to work on", metavar="IDS")
	settings.add_option("-p", "--posts", type="list", help="post IDs to work on", metavar="IDS")
	settings.add_option("--forums", type="list", help="forum IDs to restrict the bot to", metavar="IDS")
	settings.add_option("--limit", type="bool", default=False, dest="limit_posts", help="stop posting in threads in which we've posted too much")
	settings.add_option("--match", type="bool", default=False, reverse="--skip-match", dest="match_quotes", help="require a quote match")

def main(me, settings, options):
	# Forums allowed to post in
	forums = []
	if options.forums:
		for forum in options.forums:
			# Don't error out when a non-integer is found
			try:
				forums.append(int(forum))
			except ValueError:
				pass
	
	# Use new posts or threads passed by argument?
	use_new_posts = None
	threads = []
	posts = []
	
	if options.threads:
		for thread in options.threads:
			try:
				threads.append(forumapi.Thread(me, id=int(thread)))
			except:
				pass
		
		count = len(threads)
		use_new_posts = False
	elif options.posts:
		for post in options.posts:
			try:
				posts.append(forumapi.Post(me, id=int(post)))
			except:
				pass

		count = len(posts)
		use_new_posts = False
	else:
		use_new_posts = True
		if me.site.type == "vanilla":
			raise(forumapi.OptionError("Vanilla cannot find new posts and requires a list of thread IDs."))
	
	if options.phrases:
		get_phrases(options.phrases)
	else:
		for phrases_file in _phrases_files:
			get_phrases(phrases_file)
	
	## Variables ##
	
	# Time in seconds to wait between attempting to make posts
	minsleep = 300
	maxsleep = 1800
	# The exact middle of the delay
	midsleep = int((minsleep + maxsleep) / 2)
	# Tolerance to use in the delay calculation
	tolerance = 60
	# Posts per page
	pppage = 25
	
	# Last delay
	delay = midsleep
	
	# Force a refresh of the new posts?
	force_refecth = False
	
	## The Real Stuff ##
	
	thread = None
	post = None
	
	x = 0
	while use_new_posts or x < count:
		# New Posts refetching
		if use_new_posts and (x == count or force_refetch):
			print("Fetching New Posts...")
			
			x = 0
			threads = me.get_search_results("new")
			count = len(threads)
			
			if count == 0:
				print("No threads to post in! Delaying for {0} seconds...".format(delay))
				time.sleep(delay)
				continue
			
			print("Got {0} threads to post in!".format(count))
			force_refetch = False
		
		x += 1
		
		if phrases == "all":
			cname = random.choice(people)
		else:
			cname = phrases
		
		if posts:
			post = random.choice(posts)
		else:
			thread = random.choice(threads)
			print(repr(thread))
		
		# Start posting stuff
		try:
			if use_new_posts and thread is not None:
				# TODO: Does not allow posting in subforums of forums we are
				# allowed to post in, do we want that?
				if forums and all((forum != thread.forum.id) for forum in forums):
					raise(forumapi.PermissionError("Attempting to post in forum we're not allowed to post in."))
				
				if options.limit_posts and hasattr(thread, "_own_posts"):
					try:
						print("Posts: {0}".format(thread.post_count))
						print("Own posts: {0}".format(thread.own_posts))
						if thread.own_posts >= math.ceil(thread.post_count / pppage):
							#skip_markread = True
							raise(forumapi.ForumAPIException("Too many own posts in this thread."))
					except NotImplementedException:
						pass
			
			if me.site.type == "vanilla":
				prepared_post = me.prepare_post(t=thread, p=post)
				prepared_post.message = random.choice(_phrases[cname].split("\n"))
			else:
				if not post:
					post = thread.latest_post
				
				prepared_post = me.prepare_post(p=post)
				print(repr(post))
				
				if options.match_quotes:
					# TODO: Shouldn't this be in the forumapi?
					quote = forumapi.convert_entities(post.content)
					print(quote)
					
					matchable_text = re.split('\W+', quote)
					
					attempts = 0
					text = False
					max_len = 100
					match_len = min(max_len, len(matchable_text))
					
					while text == False and attempts < match_len:
						match_text = random.choice(matchable_text)
						matchable_text.remove(match_text)
						test_text = str(match_text).lower()
						
						if match_is_good(test_text) == True:
							print("Possible match: " + test_text)
							text = search_phrases(cname, test_text)
						
						attempts += 1
					
					if text == 0:
						raise(forumapi.ForumAPIException("Couldn't find anything useful."))
					
					print(text)
					
					# Bolden match
					quote = re.sub("({0})".format(re.escape(match_text)), bolden_match, quote)
					
					prepared_post.message = "[quote={0};{1}]{2}[/quote]{3}".format(post.user.name, post.id, quote, text)
				else:
					prepared_post.message += random.choice(_phrases[cname].split("\n"))
			
			result = prepared_post.submit()
			# Make sure that we don't miss out on new posts
			force_refetch = True
		except KeyboardInterrupt:
			raise
		except Exception as e:
			print(e)
			result = None
			force_refetch = False
		finally:
			print(repr(result))
		
		# Prevent double posting
		if threads:
			threads.remove(thread)
		else:
			posts.remove(post)
		
		# Set a delay between post attempts. How this works:
		# If previous delay is lower than the middle, the next delay will be
		# forced to be higher.
		# If it's higher than the middle, force the next delay to be lower.
		# If the delay fits in a small timeframe around the middle, allow the
		# next delay to be anything.
		# TODO: More random delays
		if delay < (midsleep - tolerance):
			delay = random.randint(delay, maxsleep)
		elif delay > (midsleep + tolerance):
			delay = random.randint(minsleep, delay)
		else:
			delay = random.randint(minsleep, maxsleep)
		
		# Mark the thread as read so it doesn't show up in newest posts anymore.
		if use_new_posts:
			thread.mark_read()
		
		if result and x < count:
			print("Delaying for {0} seconds...".format(delay))
			time.sleep(delay)

if __name__ == "__main__":
	# TODO: Make sure that Vanilla works
	forumapi.run(main, options=settings, supported_types=["vbulletin", "vanilla"])
